﻿using CommonEngine;
using CommonFP;
using CommonFP.Enc;
using System;
using System.Collections.Immutable;
using System.Threading.Tasks;

namespace CommonDevelop.SSO
{
    public abstract class SSOCommand
    {
        public DateTime Timestamp { get; set; }

        public T WithTimestamp<T>(DateTime timestamp) where T : SSOCommand
        {
            T result = (T)this.MemberwiseClone();
            result.Timestamp = timestamp;
            return result;
        }
    }

    public abstract class SSO_Msg
    {
        public string SId { get; set; }
    }

    #region--单点内部--命令与事件--

    /// <summary>
    /// 注销用户
    /// </summary>
    public class LogoutUserCommand : SSOCommand
    {

    }

    /// <summary>
    /// 激活用户
    /// </summary>
    public class ActivateUserCommand : SSOCommand
    {

    }

    /// <summary>
    /// 检查用户
    /// </summary>
    public class CheckingUserCommand : SSOCommand
    {
        public string Token { get; set; }
    }

    /// <summary>
    /// 检查用户
    /// </summary>
    public class CheckUserCommand : SSOCommand
    {

    }

    /// <summary>
    /// 查询用户事件
    /// </summary>
    public class LookupUser_Msg : SSO_Msg
    {

    }

    /// <summary>
    /// 刷新用户事件
    /// </summary>
    public class RefreshUser_Msg : SSO_Msg
    {
        public UserSSO UserState { get; set; }
    }

    #endregion

    /// <summary>
    /// 用户单点状态
    /// </summary>
    public struct UserSSO
    {
        public string UserID { get; }
        public string Token { get; }
        public string BusinessID { get; }
        public DateTime Expiration { get; }
        public bool HasValid { get; }
        /// <summary>
        /// 构造
        /// </summary>
        public UserSSO(string userId, string token, string BId, bool Valid = true)
        {
            this.UserID = userId;
            this.Token = token;
            this.BusinessID = BId;
            this.HasValid = Valid;
            this.Expiration = DateTime.UtcNow.AddMinutes(60 * (Valid ? 3 : -3));
        }

        public UserSSO WithLogout(LogoutUserCommand Command)
            => new UserSSO(userId: this.UserID,
                token: string.Empty,
                BId: this.BusinessID,
                Valid: false);

        public UserSSO WithActivate(ActivateUserCommand Command)
            => new UserSSO(userId: this.UserID,
                token: CommonOP.TokenEncrypt(this.UserID, this.BusinessID),
                BId: this.BusinessID,
                Valid: true);

        /// <summary>
        /// 状态“充血”，验证用户缓存数据
        /// </summary>
        public UserSSO WithChecking(CheckingUserCommand Command)
            => new UserSSO(userId: this.UserID,
                token: this.Token.Equals(Command.Token) ? this.Token : CommonOP.TokenEncrypt(this.UserID, this.BusinessID),
                BId: this.BusinessID,
                Valid: DateTime.UtcNow < this.Expiration);

        public UserSSO WithCheck(CheckUserCommand Command)
            => new UserSSO(userId: this.UserID,
                token: this.Token,
                BId: this.BusinessID,
                Valid: this.HasValid);

        public override string ToString()
            => $"{this.UserID}:{this.Token}:{this.BusinessID}";
    }

    /// <summary>
    /// 单点行为过程
    /// </summary>
    public class SSOProcess
    {
        private Agent<SSOCommand, Validation<UserSSO>> agent;
        /// <summary>
        /// 构造
        /// </summary>
        public SSOProcess(UserSSO initialState, Func<UserSSO, Task<ValueTuple>> saveLog)
        {
            //双向代理
            this.agent = Agent.Start(initialState,
                async (UserSSO state, SSOCommand msg) =>
                {
                    Validation<UserSSO> result = new Pattern
                    {
                        (LogoutUserCommand Command) => state.WithLogout(Command),
                        (ActivateUserCommand Command) => state.WithActivate(Command),
                        (CheckingUserCommand Command) => state.WithChecking(Command),
                        (CheckUserCommand Command) => state.WithCheck(Command),
                    }.Match(msg);

                    //这里saveAndPublish(Event)被换成了状态日志，正常情况是由命令生成事件，对事件进行“保存，事件库”
                    //新的状态UserSSO，将会回归到SSOProcess的agent实例的缓存中
                    await result.Traverse(tpl => saveLog(tpl));

                    var newState = result.GetOrElse(state);
                    return (newState, result);
                });
        }

        public async Task<Validation<UserSSO>> Handle(SSOCommand cmd)
             => await agent.Tell(cmd);
    }

    /// <summary>
    /// 单点登录注册
    /// </summary>
    public class SSORegistry
    {
        Agent<SSO_Msg, Option<SSOProcess>> agent;  //定义两个类型就一定是要回复了
        Func<string, Task<Option<UserSSO>>> LoadUser;

        /// <summary>
        /// 构造
        /// </summary>
        public SSORegistry(Func<string, Task<Option<UserSSO>>> LoadingUser, Func<UserSSO, Task<ValueTuple>> saveLog)
        {
            this.LoadUser = LoadingUser;

            this.agent = Agent.Start(ImmutableDictionary<string, SSOProcess>.Empty, //用户缓存作为状态
                (ImmutableDictionary<string, SSOProcess> cache, SSO_Msg msg)
                => new Pattern<(ImmutableDictionary<string, SSOProcess>, Option<SSOProcess>)>
                {
                    (LookupUser_Msg m) => (cache, cache.Lookup(m.SId)),
                    (RefreshUser_Msg m) => cache.Lookup(m.SId).Match(
                        Some: acc => (cache, acc.ToOption()),
                        None: () =>
                        {
                            var account = new SSOProcess(m.UserState, saveLog);
                            return (cache.SetItem(m.SId, account), account.ToOption());
                        })
               }.Match(msg));
        }

        public Task<Option<SSOProcess>> Lookup(string id)
            => agent
            .Tell(new LookupUser_Msg { SId = id })
            .OrElse(() =>
              from state in this.LoadUser(id) // loading the state is done in the calling thread
              from account in agent.Tell(new RefreshUser_Msg
              {
                  SId = id,
                  UserState = state
              })
              select account);
    }
}
